/************************************************************************
* net_udp.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#include "file.h"
#include "list.h"
#define _NET_LOCAL
#include "net.h"

#include <string.h>
#ifndef WIN32
#include <unistd.h>
#include <errno.h>
#endif

/* create a new udp client connection */
net_connection_t *udp_client_connect(char* host, char* port)
{
	net_connection_t *n = net_connection();
	n->type = NETTYPE_UDP;
	n->host = strdup(host);
	n->port = strdup(port);

	udp_client_reconnect(n);

	return n;
}

/* reconnect a close udp connection */
int udp_client_reconnect(net_connection_t *n)
{
	int max_retries = config_get_int("net.max_retries");
	if (!max_retries)
		max_retries = 5;
	if (n->state == NETSTATE_OPEN || n->tries > max_retries)
		return 2;

	n->tries++;

	if (n->state == NETSTATE_UNUSED) {
		int f = config_get_int("net.ipv4");
		int s = config_get_int("net.ipv6");
		memset(&n->hints, 0, sizeof(n->hints));
		if (s == f) {
			n->hints.ai_family = AF_UNSPEC;
		}else if (s) {
			n->hints.ai_family = AF_INET6;
		}else if (f) {
			n->hints.ai_family = AF_INET;
		}
		n->hints.ai_socktype = SOCK_DGRAM;

		/* resolve hostname */
		if (getaddrinfo(n->host, n->port, &n->hints, &n->addr)) {
			vlprintf(CN_ERROR, "Unable to resolve host '%s'",n->host);
			return 1;
		}

		n->state = NETSTATE_CLOSED;
	}

	/* open socket */
	if ((n->fd = socket(n->addr->ai_family, n->addr->ai_socktype, n->addr->ai_protocol)) == -1) {
		vlprintf(CN_ERROR, "Unable to reconnect to host %s",n->host);
		return 1;
	}

	/* connect to server */
	if (connect(n->fd, n->addr->ai_addr, n->addr->ai_addrlen)) {
		shutdown(n->fd,2);
		vlprintf(CN_ERROR, "Unable to reconnect to host %s",n->host);
		return 1;
	}

	n->state = NETSTATE_OPEN;
	n->tries = 0;

	return 0;
}

/* create a new udp server connection */
net_connection_t *udp_host_connect(char* host, char* port)
{
	net_connection_t *n = net_connection();
	if (!host)
		host = "*";

	n->type = NETTYPE_UDP_HOST;
	n->tries = 0;
	n->host = strdup(host);
	n->port = strdup(port);
	n->peers = array_create(ARRAY_TYPE_PTR);

	if (udp_host_reconnect(n)) {
		net_close(n);
		return NULL;
	}

	return n;
}

/* start listening for udp clients */
int udp_host_reconnect(net_connection_t *n)
{
	int max_retries = config_get_int("net.max_retries");
	if (!max_retries)
		max_retries = 5;
	if (n->state == NETSTATE_OPEN || n->tries > max_retries)
		return 2;

	n->tries++;

	if (n->state == NETSTATE_UNUSED) {
		char* nhost = NULL;
		int f = config_get_int("net.ipv4");
		int s = config_get_int("net.ipv6");

		memset(&n->hints, 0, sizeof(n->hints));
		if (s == f) {
			n->hints.ai_family = AF_UNSPEC;
		}else if (s) {
			n->hints.ai_family = AF_INET6;
		}else if (f) {
			n->hints.ai_family = AF_INET;
		}
		n->hints.ai_socktype = SOCK_DGRAM;
		n->hints.ai_flags = AI_PASSIVE;

		if (strcmp(n->host,"*"))
			nhost = n->host;

		/* resolve hostname */
		if (getaddrinfo(nhost, n->port, &n->hints, &n->addr)) {
			vlprintf(CN_ERROR, "Unable to resolve host '%s'",n->host);
			return 1;
		}

		n->state = NETSTATE_CLOSED;
	}

	/* open socket */
	if ((n->fd = socket(n->hints.ai_family, n->hints.ai_socktype, n->hints.ai_protocol)) == -1) {
		vlprintf(CN_ERROR, "Unable to open port %u",n->port);
		return 1;
	}
	/* bind to the socket */
	if ((bind(n->fd, n->addr->ai_addr,sizeof(*n->addr->ai_addr))) < 0) {
		vlprintf(CN_ERROR, "Unable to bind port %u",n->port);
		return 1;
	}

	n->state = NETSTATE_OPEN;
	n->tries = 0;

	return 0;
}

/* write data to a udp connection */
int udp_write(net_connection_t *n, void *buff, unsigned int size)
{
	int r = 0;
	if (n->state == NETSTATE_OPEN) {

		if (n->type == NETTYPE_UDP_HOST) {
			r = (int)sendto(n->fd, buff, size, 0, (struct sockaddr*)&n->remote_addr, n->remote_addr_len);
		}else{
			r = (int)write(n->fd,buff,size);
		}
		if (r < 1) {
			vlprintf(CN_ERROR, "failed to write to connection %d (%d)",n->fd,errno);
			if (n->type != NETTYPE_UDP_HOST)
				shutdown(n->fd,2);
			n->state = NETSTATE_CLOSED;
		}
	}
	return r;
}

static int udp_process(net_connection_t *n)
{
	char buff[2048];
	int l;

	errno = 0;
	if (n->type == NETTYPE_UDP_HOST) {
		l = recvfrom(n->fd, buff, 2048, 0, (struct sockaddr *)&n->remote_addr, &n->remote_addr_len);
	}else{
		l = (int)recv(n->fd,buff,2048,0);
	}

	if (l < 1) {
#ifndef WIN32
		if (errno != EAGAIN && errno != EWOULDBLOCK) {
			if (n->type != NETTYPE_UDP_HOST)
				shutdown(n->fd,2);
			n->state = NETSTATE_CLOSED;
		}
#else
		if (n->type != NETTYPE_UDP_HOST)
			shutdown(n->fd,2);
		n->state = NETSTATE_CLOSED;
#endif
	}else{
		file_t *f = file_create(NULL,NULL);
		file_write(f,buff,l);
		n->buff.udp.unsorted = list_push(&n->buff.udp.unsorted,f);
	}

	/* TODO: sort the unsorted packets */

	return 0;
}

/* discards the current packet buffer, and returns a pointer to the next packet buffer */
file_t *udp_next(net_connection_t *n)
{
	file_t *f;
	if (n->state != NETSTATE_OPEN)
		return NULL;

	udp_process(n);

	if (!n->buff.udp.sorted)
		return NULL;

	f = list_pull(&n->buff.udp.sorted);
	if (f)
		file_free(f);

	return n->buff.udp.sorted;
}

/* gets a pointer to the current packet buffer */
file_t *udp_current(net_connection_t *n)
{
	if (n->state != NETSTATE_OPEN)
		return NULL;

	udp_process(n);

	return n->buff.udp.sorted;
}

/* determine if data is pending on a udp connection */
int udp_pending(net_connection_t *n)
{
	if (n->state != NETSTATE_OPEN)
		return 0;

	udp_process(n);
	return 0;
}

/* read data from a udp connection */
int udp_read(net_connection_t *n, void *buff, unsigned int size)
{
	file_t *b;
	int l;

	b = udp_current(n);
	if (!b)
		return 0;

	l = file_read(b,buff,size);
	if (l > 0)
		return l;

	b = udp_next(n);
	if (!b)
		return 0;

	return file_read(b,buff,size);
}

/* read a line from a udp connection */
int udp_readline(net_connection_t *n, void *buff, unsigned int size)
{
	file_t *b;
	int l;

	b = udp_current(n);
	if (!b)
		return 0;

	l = file_readline(b,buff,size);
	if (l > 0)
		return l;

	b = udp_next(n);
	if (!b)
		return 0;

	return file_readline(b,buff,size);
}

/* broadcast data to all peers of a udp host connection */
int udp_broadcast(net_connection_t *n, void *buff, unsigned int size)
{
	int i;
	net_connection_t **p;
	if (!n || n->state != NETSTATE_OPEN || !n->peers || !n->peers->length)
		return 0;

	p = n->peers->data;
	for (i=0; i<n->peers->length; i++) {
		if (!p[i])
			continue;
		udp_write(p[i],buff,size);
	}

	return i;
}

/* accept new connections to a udp host */
int udp_accept(net_connection_t *n)
{
	net_connection_t *c;
	struct sockaddr_in *s4[2];
	struct sockaddr_in6 *s6[2];
	char buff[2048];
	int l;
	array_t *a = net_select(0,0,n);
	if (!a)
		return -1;

	if (!a->length) {
		array_free(a,1);
		return -1;
	}

	c = net_connection();
	c->type = n->type;
	c->state = NETSTATE_OPEN;
	c->fd = n->fd;
	c->buff.udp.unsorted = NULL;
	c->buff.udp.sorted = NULL;

	l = recvfrom(n->fd, buff, 2048, 0, (struct sockaddr *)&c->remote_addr, &c->remote_addr_len);

	if (l > -1) {
		int i;
		net_connection_t **p = n->peers->data;
		file_t *f = file_create(NULL,NULL);
		file_write(f,buff,l);
		c->buff.udp.unsorted = list_push(&c->buff.udp.unsorted,f);
		for (i=0; i<n->peers->length; i++) {
			if (p[i] && c->remote_addr.ss_family == p[i]->remote_addr.ss_family) {
				if (c->remote_addr.ss_family == AF_INET) {
					s4[0] = (struct sockaddr_in*)&c->remote_addr;
					s4[1] = (struct sockaddr_in*)&p[i]->remote_addr;
					if (s4[0]->sin_addr.s_addr == s4[1]->sin_addr.s_addr) {
						c->fd = -1;
						net_close(c);
						return i;
					}
				}else if (c->remote_addr.ss_family == AF_INET6) {
					s6[0] = (struct sockaddr_in6*)&c->remote_addr;
					s6[1] = (struct sockaddr_in6*)&p[i]->remote_addr;
					if (!memcmp(s6[0]->sin6_addr.s6_addr,s6[1]->sin6_addr.s6_addr,16)) {
						c->fd = -1;
						net_close(c);
						return i;
					}
				}
			}
		}
		for (i=0; i<n->peers->length; i++) {
			if (!p[i]) {
				p[i] = c;
				return i;
			}
		}
		array_push_ptr(n->peers,c);
		return n->peers->length-1;
	}

	c->fd = -1;
	net_close(c);
	return -1;
}
